<?php
/**
 * Smart Library - Smart Framework
 * http://smart-framework.highet.eu
 *
 * @category   SmartL
 * @package    SmartL_Config
 * @subpackage Storage
 * @copyright  Copyright (c) 2005-2008 Smart Framework team - Stepan Sindelar, Vojtech Vit
 * @license    http://www.opensource.org/licenses/bsd-license.php	New BSD License
 */

/**
 * pozn. je Serializable nutné? viz testy, tam demonstruji použití serializace na tento objekt i bez tohoto rozhraní.
 *
 * Basic unit of configuration-one configuration node with its children.
 *
 * @todo : Je třeba definovat jasnou strukturu konfiguračních souborů. Např. nastavení složek překladů je teď dost chaotický.
 * @package SmartL_Config
 * @subpackage Storage
 */
class SmartL_Config_Storage implements Countable, ArrayAccess, Iterator, Serializable {
	/**
	 * Attribute that is used to define rewrite mode
	 */
	const REWRITE_MODE_ATTRIBUTE = 'rewriteMode';
	/**
	 * Rewrite mode "None" means that tags of the same name will create arrays
	 * instead of rewriting themselves.
	 */
	const REWRITE_MODE_NONE = 'none';
	/**
	 * If you set rewrite mode "List" to some tag, then all it's child tags may be
	 * only of add/remove type and must contain index attribute. All "extending"
	 * tags will then be able to change it's value using only these add/remove
	 * child tags.
	 */
	const REWRITE_MODE_LIST = 'list';
	/**
	 * If you set rewrite mode "Rewrite" to some tag, then all tags with the same
	 * name will rewrite it's value. It's child tags, however, will remain and
	 * those "new" will be appended to them.
	 */
	const REWRITE_MODE_REWRITE = 'rewrite';
	/**
	 * If you set rewrite mode "Rewrite" to some tag, then all tags with the same
	 * name will rewrite it's value as well as it's child tags.
	 */
	const REWRITE_MODE_DEEPREWRITE = 'deepRewrite';
	/**
	 * Attribute that is used as array index when more same-name tags are direct
	 * children of one parent node. It's usage is forced in "configuration lists"
	 * defined by rewrite mode = "list".
	 */
	const INDEX_ATTRIBUTE = 'id';
	
	private $_value = null;
	private $_rewriteMode = self::REWRITE_MODE_NONE;
	protected $_children = null;
	
	/**
	 * Returns content of this node. Defaultly it returns reference to the
	 * same node ($this), but this behavior may be changed in some special
	 * derived classes.
	 * It is used when parent want to access data from its child in magic getter.
	 *
	 * @todo 1 : (S) Jaký je přesně vztah mezi getData, getValue a getChildren?
	 * Pozn:
	 * getValue - vrátí hodnotu toho nodu, takže <item> neco <subitem/></item>
	 * je getValue() == neco
	 * getData() == this
	 * getChildren() == array, kde je subitem
	 *
	 * @return SmartL_Config_Storage Data of this node
	 */
	public function getData() {
		return $this;
	}
	/**
	 * Returns text value of this node. It returns NULL if no value was set
	 * (even though some child tags exist).
	 *
	 * @return string Text value of this node
	 */
	public function getValue() {
		return $this->_value;
	}
	/**
	 * Helper method for retrieving boolean values.
	 * It returns strtolower(value) == 'true'.
	 *
	 * @return boolean
	 */
	public function getBooleanValue() {
		return strtolower ( $this->getValue () ) == 'true';
	}
	/**
	 * Sets text value of this node.
	 *
	 * @param string $value New text value of this node
	 */
	public function setValue($value) {
		$this->_value = $value;
	}
	/**
	 * Returns child nodes of this node.
	 *
	 * @return array [string Index|numeric] => SmartL_Config_Storage
	 */
	public function getChildren() {
		return $this->_children;
	}
	/**
	 * Returns data of specified child node.
	 *
	 * @param string $name Child node name
	 * @return SmartL_Config_Storage|array Returns requested child (when array,
	 * it's [string Index|numeric] => SmartL_Config_Storage)
	 */
	public function __get($name) {
		if (! isset ( $this->_children [$name] )) {
			require_once 'SmartL/Config/Storage/Exception.php';
			throw new SmartL_Config_Storage_Exception ( "Requested children '{$name}' doesn't exist." );
		}
		if (is_array ( $this->_children [$name] )) {
			return $this->_children [$name];
		}
		return $this->_children [$name]->getData ();
	}
	/**
	 * Returns whether given child node is set.
	 *
	 * @param string $name Child node name
	 * @return bool True if given child node is set
	 */
	public function __isset($name) {
		return isset ( $this->_children [$name] );
	}
	/**
	 * Returns rewrite mode of this node.
	 *
	 * @return strign Rewrite mode (constant of SmartL_Config_Storage class)
	 */
	public function getRewriteMode() {
		return $this->_rewriteMode;
	}
	/**
	 * Returns rewrite mode of given child node.
	 *
	 * @todo 4 : (S) Tato funkce mi přijde nesystematická.
	 * Pozn: Tato funkce slouzi pouze jako helper, aby se nemuselo zjistovat, jakeho typu je pozadovany child
	 * @deprecated
	 *
	 * @return strign Rewrite mode (constant of SmartL_Config_Storage class)
	 */
	public function getChildRewriteMode($childName) {
		if (! isset ( $this->_children [$childName] )) {
			require_once 'SmartL/Config/Storage/Exception.php';
			throw new SmartL_Config_Exception ( "Child with name {$childName} doesn't exist." );
		}
		
		if (is_array ( $this->_children [$childName] )) {
			return reset ( $this->_children [$childName] )->getRewriteMode ();
		} else {
			return $this->_children [$childName]->getRewriteMode ();
		}
	}
	/**
	 * Sets rewrite mode of this node.
	 *
	 * @param string $value New rewrite mode (constant of SmartL_Config_Storage class)
	 */
	public function setRewriteMode($value) {
		$this->_rewriteMode = $value;
	}
	/**
	 * Adds child node to this node.
	 *
	 * @param string $name Child node name
	 * @param SmartL_Config_Storage $child Child node data
	 */
	public function addChild($name, SmartL_Config_Storage $child) {
		$index = $child->getIndex ();
		if ($index !== null) {
			$this->_children [$name] [$index] = $child;
		} else {
			if (isset ( $this->_children [$name] ) && ! is_array ( $this->_children [$name] )) {
				$this->_children [$name] = array ($this->_children [$name] );
			}
			if (isset ( $this->_children [$name] )) {
				$this->_children [$name] [] = $child;
			} else {
				$this->_children [$name] = $child;
			}
		}
	}
	/**
	 * Returns values of child node with given name
	 *
	 * @todo 3 : (S) Tato funkce mi přijde nesystematická.
	 * Pozn: Je to dalsi pomocna funkce, ktera zajistuje jednotnou navratovou hodnotu
	 * @deprecated
	 *
	 * @param string $childName Child node name
	 * @return array [string Index|numeric] => string
	 */
	public function getChildValues($childName) {
		$result = array ();
		if (isset ( $this->_children [$childName] )) {
			if (is_array ( $this->_children [$childName] )) {
				$array = $this->_children [$childName];
			} else {
				$array = array ($this->_children [$childName] );
			}
			foreach ( $array as $index => $data ) {
				$result [$index] = $data->getValue ();
			}
		}
		return $result;
	}
	/**
	 * Returns value of "index attribute" (defined by constant INDEX_ATTRIBUTE).
	 * Returns null when index attribute is not set.
	 *
	 * @return string|null Index attribute or NULL
	 */
	public function getIndex() {
		if (isset ( $this->_children [self::INDEX_ATTRIBUTE] )) {
			return $this->_children [self::INDEX_ATTRIBUTE]->getValue ();
		}
		return null;
	}
	/**
	 * Helper returns index of node. The same as getIndex in storage.
	 *
	 * @param DOMNode $node
	 * @return string
	 */
	static public function getNodeIndex(DOMNode $node) {
		if ($node->hasAttributes ()) {
			foreach ( $node->attributes as $attribute ) {
				if ($attribute->name == self::INDEX_ATTRIBUTE)
					return $attribute->value;
			}
		}
		return null;
	}
	/**
	 * Removes all child nodes of this node.
	 *
	 */
	public function clearChildren() {
		$this->_children = null;
	}
	/**
	 * Returns whether this node has some child nodes.
	 *
	 * @return boolean True if this node has some child nodes.
	 */
	public function hasChildren() {
		return $this->_children != null;
	}
	/**
	 * Recursively returns this node's data as an associative array.
	 * @todo 2 : (S) toArray() - upřesnit popis metody
	 * If some node(1) has value and also child nodes, in array result of this function will
	 * be under index-name of node(1) array with values of its children.
	 *
	 * @param bool $convertNullToArray If set to true, NULL values will be
	 *                                 converted to empty arrays
	 * @return array [string Index] => (string Node Value
	 *               | array [string Child Index] => string Child value)
	 */
	public function toArray($convertNullToArray = true) {
		if ($this->_children === null)
			return array ();
		
		$result = array ();
		foreach ( $this->_children as $key => $child ) {
			if (is_array ( $child )) {
				//If it is array, in resutl it will be also array
				foreach ( $child as $key2 => $child2 ) {
					if ($child [$key2]->hasChildren ()) {
						$result [$key] [$key2] = $child2->toArray ();
					} else {
						$result [$key] [$key2] = $child2->getValue ();
					}
				}
			} else {
				//If it has children
				if ($child->hasChildren ()) {
					$result [$key] = $child->toArray ( $convertNullToArray );
				} else {
					$value = $child->getValue ();
					if ($value == "") {
						if ($convertNullToArray) {
							$value = array ();
						} else {
							$value = null;
						}
					}
					$result [$key] = $value;
				}
			}
		}
		return $result;
	}
	
	//------------------------------------//
	//Implemented from interfaces
	

	/**
	 * Implementation of Countable inteface.
	 * @return integer Count of children.
	 */
	public function count() {
		return count ( $this->_children );
	}
	
	/**
	 * Implementation of ArrayAccess interface.
	 * @param string $key Name of children.
	 * @return boolean True if child with given name exists.
	 */
	public function offsetExists($key) {
		return isset ( $this->_children [$key] );
	}
	
	/**
	 * Implementation of ArrayAccess interface.
	 * Unset child with given name.
	 * @param string $key Name of children.
	 */
	public function offsetUnset($key) {
		unset ( $this->_children [$key] );
	}
	
	/**
	 * Implementation of ArrayAccess interface.
	 * @param string $key Name of children.
	 * @return SmartL_Config_Storage|array[string|numeric]=>SmartL_Config_Storage Child|children with given name.
	 */
	public function offsetGet($key) {
		return $this->_children [$key];
	}
	
	/**
	 * Implementation of ArrayAccess interface.
	 * @param string $key Name of children.
	 * @param array[string|numeric]=>SmartL_Config_Storage|SmartL_Config_Storage $value new value of given offset.
	 */
	public function offsetSet($key, $value) {
		$this->_children [$key] = $value;
	}
	
	/**
	 * Implementation of Iterator interface.
	 * Resets internal pointer to first child.
	 */
	public function rewind() {
		reset ( $this->_children );
	}
	
	/**
	 * Implementation of Iterator interface.
	 * Moves internal pointer to next child.
	 * @return SmartL_Config_Storage|array[string|numeric]=>SmartL_Config_Storage The next child.
	 */
	public function next() {
		return next ( $this->_children );
	}
	
	/**
	 * Implementation of Iterator interface.
	 * @return SmartL_Config_Storage|array[string|numeric]=>SmartL_Config_Storage Current child.
	 */
	public function current() {
		return current ( $this->_children );
	}
	
	/**
	 * Implementation of Iterator interface.
	 * @return string Key=name of current child.
	 */
	public function key() {
		return key ( $this->_children );
	}
	
	/**
	 * Implementation of Iterator interface.
	 * @return boolean True, if the internal pointer is on valid position.
	 */
	public function valid() {
		return current ( $this->_children ) !== false;
	}
	
	/**
	 * @todo Not implemented
	 *
	 * @return string
	 */
	public function serialize() {
		$result = $this->_value . ';' . $this->_rewriteMode . ';';
		if (count ( $this->_children ) > 0)
			$result .= '<' . serialize ( $this->_children ) . ">";
		return $result;
	}
	
	/**
	 * Helper method for serialization
	 *
	 * @param string $string
	 * @param string|char $delimer
	 * @return string
	 */
	protected function _next(&$string, $delimer = ';') {
		$result = substr ( $string, 0, strpos ( $string, $delimer ) );
		$string = substr ( $string, strpos ( $string, $delimer ) + 1 );
		return $result;
	}
	
	/**
	 * @todo Not implemented
	 *
	 * @param unknown_type $serialized
	 */
	public function unserialize($serialized) {
		$this->_value = $this->_next ( $serialized );
		$this->_rewriteMode = $this->_next ( $serialized );
		$this->_next ( $serialized, '<' );
		$serialized = substr ( $serialized, 0, strlen ( $serialized ) - 1 );
		if (strlen ( $serialized ) == 0)
			return;
		$this->_children = unserialize ( $serialized );
	}
}
